/*
 * Licensed to the Tai Ping Jin Ke
 *
 * Copyright (c) 2022 .
 * All rights reserved.
 * 项目名称：登录通-业务逻辑层
 * 版权说明：本软件属太平金融科技服务(上海)有限公司所有，在未获得太平金融科技服务(上海)有限公司正式授权情况下，任何企业和个人，不能获取、阅读、安装、传播本软件涉及的任何受知识产权保护的内容。
 */
package com.taiping.dlt.service;

import com.taiping.dlt.application.constant.dao.DataSourceEnum;
import com.taiping.dlt.base.BaseService;
import com.taiping.dlt.client.service.AuthorizeServiceClient;
import com.taiping.dlt.converter.AuthLoginBOConverter;
import com.taiping.dlt.converter.AuthLoginDOConverter;
import com.taiping.dlt.converter.AuthLoginQOConverter;
import com.taiping.dlt.dataobject.AuthLoginDO;
import com.taiping.dlt.pojo.bo.AuthLoginBO;
import com.taiping.dlt.pojo.query.AuthLoginQO;
import com.taiping.dlt.repository.IRepository;
import com.taiping.dlt.strategy.RepositoryHandlerContext;
import com.taiping.dlt.strategy.RepositoryHandlerFactory;
import com.taipingframework.utility.exception.SystemInterrupterException;
import com.taipingframework.boot.web.response.status.ApiStatusEnum;
import com.taipingframework.boot.web.security.SecretHandler;
import com.taipingframework.utility.constant.ApplicationConstant;
import com.taipingframework.utility.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

@Slf4j
@Service
public class DltService extends BaseService {
//CHECKSTYLE:OFF

    /**
     * 登录接口
     */
    public Mono<AuthLoginBO> login(AuthLoginBO authLoginBO) {
        //TODO - 在企业统一认证中心登录之后，将会携带票据ticket跳转到当前系统的前端页面，前端页面调用后端登录接口时会将票据ticket进一步传递。
        // 此处拿到票据之后，依据该参数去企业统一认证中心识别登录人的唯一凭证
        // 然后，依据该凭证创建本地会话

        // 1、查询数据库，判断账户是否存在

        // 2、验证账户密码的秘文是否一致

        // 3、查询数据库，获取用户权限列表。
        // ------（1）每个子系统可能都有自己的权限管理；
        // ------（2）权限管理模块可能集成到了`企业统一认证中心`；
        // ------（3）权限管理模块可能作为一个独立的项目在开发。
        // 无论哪种情况，此处都无法直接获取用户的权限列表，只能提供接口：
        // 然后，在gateway这一层实现接口方法，调用`企业统一认证中心`或者外部`用户权限管理项目`的open-api，
        // 再或者，是查询某个子项目自己的数据库、mongodb、es、redis等。
        // 总之，此处一定要拉取到用户的权限列表才能放行，否则，不允许执行下面的代码进行创建本地会话。

        // 4、使用spring-session创建本地会话(或者使用jwt创建token)
        // 此处需要补充下：如果使用jwt，禁止将用户的权限列表、用户密码等敏感信息放入token之中：
        // 首先，token的长度容易过渡泛滥；
        // 其次，token会被交付给客户端进行保管，有被破解的安全风险。

        // 5、将用户权限列表存放到spring-session中，如果使用了jwt，则需要直接调用redis-api执行缓存操作。

        // 6、返回token
        if (DataSourceEnum.NONE != dltProperty.getDataSource()) {
            IRepository<AuthLoginDO> repository = RepositoryHandlerFactory.createRepository(IRepository.class, AuthLoginDO.class);
            RepositoryHandlerContext<AuthLoginDO> repositoryHandlerContext = new RepositoryHandlerContext<>(repository);

            return repositoryHandlerContext.queryForList(AuthLoginDOConverter.convert(authLoginBO))
                    .subscribeOn(Schedulers.boundedElastic())
                    .log()
                    .switchIfEmpty(Mono.defer(() ->
                            Mono.deferWithContext(Mono::just)
                                    .map(context -> {
                                        String reqId = context.get(ApplicationConstant.RESPONSE_IDENTITY);
                                        //ServerWebExchange exchange = context.get(ServerWebExchange.class);
                                        //exchange.getAttributes().put(ApplicationConstant.RESPONSE_API_RESULT,
                                        //        JSONUtil.toJsonStr(ApiResult.<Boolean>build(reqId).setStatus(ApiStatusEnum.ERR_A0201).setResult(false)));

                                        log.error("[" + reqId + "]入参-登录名：{}，密码：{}", authLoginBO.getAccountCode(), authLoginBO.getLoginPassword());
                                        throw new SystemInterrupterException(ApiStatusEnum.ERR_A0201);
                                    })
                    ))
                    .elementAt(0)
                    .zipWith(Mono.deferWithContext(Mono::just), (authLoginDO, context) -> {
                        String secPwd = SecretHandler.encryptForChar(authLoginBO.getLoginPassword());
                        if (!StringUtils.equals(secPwd, authLoginDO.getLoginPassword())) {
                            String reqId = context.get(ApplicationConstant.RESPONSE_IDENTITY);
                            //ServerWebExchange exchange = context.get(ServerWebExchange.class);
                            //exchange.getAttributes().put(ApplicationConstant.RESPONSE_API_RESULT,
                            //        JSONUtil.toJsonStr(ApiResult.<Boolean>build(reqId).setStatus(ApiStatusEnum.ERR_A0210).setResult(false)));

                            log.error("[" + reqId + "]入参-登录名：{}，密码：{}", authLoginBO.getAccountCode(), authLoginBO.getLoginPassword());
                            throw new SystemInterrupterException(ApiStatusEnum.ERR_A0210);
                        }
                        // FIXME：用户登录成功之后，并非马上返回，此时还需要读取用户的角色信息
                        return AuthLoginBOConverter.convert(authLoginDO);
                    })
                    .onErrorResume(e -> Mono.error(new ServiceException(e)));
        } else {
            // 调用后端微服务接口，去mysql或oracle中查询用户信息。此处，需要特别说明：mysql或oracle支持reactive模式的r2dbc驱动，目前并不支持java8。
            // 至少需要JDK11以上的r2dbc驱动才能对mysql或oracle有较好的支持。因此，在这里我们直接远程异步调用后端微服务即可。
            // 当然，这样的做法会增加调用链路，在性能层面存在一定影响。所以，比较好的做法是将用户登录所需的数据
            // 从关系型数据库同步到elasticsearch、mongodb、redis中，直接在上方做校验即可。
            AuthLoginQO authLoginQO = AuthLoginQOConverter.convert(authLoginBO);
            return AuthorizeServiceClient.loginClientHandler(authLoginQO)
                    .onErrorResume(e -> Mono.error(new ServiceException(e)));
        }
    }

//CHECKSTYLE:ON
}
